package io.hellsinger.filesystem.linux.operation

import io.hellsinger.filesystem.linux.LinuxFileOperationProvider
import io.hellsinger.filesystem.linux.LinuxOperationOptions.Open
import io.hellsinger.filesystem.linux.path.LinuxPath
import io.hellsinger.filesystem.path.ObservablePath
import io.hellsinger.filesystem.path.Path
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.newSingleThreadContext
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
private val defaultContext = newSingleThreadContext("Linux Path Observer")

interface LinuxPathObserver : AutoCloseable {
    interface LinuxObservablePath<P : Path> : ObservablePath<P> {
        val cookie: Int

        operator fun component3(): Int = cookie
    }

    val paths: List<Path>

    val events: SharedFlow<ObservablePath<Path>>

    fun add(
        path: Path,
        events: Int,
    ): LinuxPathObserver

    fun remove(path: Path): LinuxPathObserver

    fun clear(): LinuxPathObserver

    override fun close()
}

class LinuxPathObserverImpl(
    context: CoroutineContext = defaultContext,
) : LinuxPathObserver {
    internal data class ObservableLinuxPath(
        val path: Path,
        val wd: Int,
        val mask: Int,
    )

    internal class LinuxObservablePathImpl(
        val cookie: Int,
        val mask: Int,
        val wd: Int,
        val name: ByteArray,
    )

    private val fileDescriptor = LinuxFileOperationProvider.initInotify(Open.NON_BLOCKING)

    private val watchDescriptors = mutableListOf<ObservableLinuxPath>()
    private val scope = CoroutineScope(context)
    override val events: SharedFlow<LinuxPathObserver.LinuxObservablePath<Path>>
        get() = pollEvents()

    override val paths: List<Path>
        get() = watchDescriptors.map(ObservableLinuxPath::path)

    override fun add(
        path: Path,
        events: Int,
    ): LinuxPathObserver {
        // Fast way: already registered
        if (watchDescriptors.any { descriptor -> descriptor.path == path }) return this

        watchDescriptors +=
            ObservableLinuxPath(
                path,
                LinuxFileOperationProvider.addObservablePath(
                    fd = fileDescriptor,
                    path = path.bytes,
                    mask = events,
                ),
                events,
            )
        return this
    }

    override fun remove(path: Path): LinuxPathObserver {
        val index =
            watchDescriptors.indexOfFirst { observable ->
                observable.path == path
            }

        if (index < 0) return this

        LinuxFileOperationProvider.removeObservablePath(
            fd = fileDescriptor,
            wd = watchDescriptors.removeAt(index).wd,
        )

        return this
    }

    override fun clear(): LinuxPathObserver {
        for (i in watchDescriptors.lastIndex downTo 0) {
            val descriptor = watchDescriptors.removeAt(i)
            LinuxFileOperationProvider.removeObservablePath(
                fd = fileDescriptor,
                wd = descriptor.wd,
            )
        }
        return this
    }

    private suspend fun getEvent() =
        suspendCoroutine { continuation ->
            continuation.resume(LinuxFileOperationProvider.pollEvent(fileDescriptor))
        }

    private fun pollEvents(): SharedFlow<LinuxPathObserver.LinuxObservablePath<Path>> =
        flow {
            while (true) {
                val event = getEvent()
                val observable = findWatchDescriptor(event.wd) ?: continue

                if (event.mask and observable.mask > 0) {
                    val indexOfNull = event.name.indexOf(0)

                    val resolvedName =
                        if (indexOfNull < 0) {
                            event.name
                        } else {
                            event.name.copyOfRange(0, indexOfNull)
                        }

                    val impl =
                        object : LinuxPathObserver.LinuxObservablePath<Path> {
                            override val cookie: Int = event.cookie
                            override val event: Int = event.mask
                            override val path: Path =
                                observable.path.resolve(LinuxPath(resolvedName))
                        }

                    emit(impl)
                }
            }
        }.shareIn(scope, SharingStarted.Eagerly, 10)

    override fun close() {
        defaultContext.close()
        watchDescriptors.removeAll { observable ->
            LinuxFileOperationProvider.removeObservablePath(fileDescriptor, observable.wd)
            true
        }
        LinuxFileOperationProvider.closeFileDescriptor(fileDescriptor)
    }

    private fun findWatchDescriptor(wd: Int): ObservableLinuxPath? = watchDescriptors.find { observable -> observable.wd == wd }
}

fun LinuxPathObserver(context: CoroutineContext = defaultContext): LinuxPathObserver = LinuxPathObserverImpl(context)
